package registries

import (
	"context"
	"sort"
	"strconv"

	"github.com/stackrox/rox/generated/storage"
	"github.com/stackrox/rox/pkg/registries/types"
	"github.com/stackrox/rox/pkg/set"
	"github.com/stackrox/rox/pkg/sync"
)

type setImpl struct {
	lock sync.RWMutex

	factory      Factory
	integrations map[string]types.ImageRegistry
	creatorOpts  []types.CreatorOption
}

func sortIntegrations(integrations []types.ImageRegistry) {
	// TODO(ROX-25474) refactor to pass parent context.
	ctx := context.Background()
	// This just ensures that the registries that have username/passwords are processed first
	sort.SliceStable(integrations, func(i, j int) bool {
		conf1 := integrations[i].Config(ctx)
		conf2 := integrations[j].Config(ctx)
		// Sort by username existing first
		if conf1.Username != "" && conf2.Username == "" {
			return true
		}
		if conf1.Username == "" && conf2.Username != "" {
			return false
		}
		// Then sort by if autogenerated or not, this should prefer non-autogenerated registries (user input over autogenerated)
		return !conf1.Autogenerated && conf2.Autogenerated
	})
}

func (e *setImpl) getSortedRegistriesNoLock() []types.ImageRegistry {
	integrations := make([]types.ImageRegistry, 0, len(e.integrations))
	for _, i := range e.integrations {
		integrations = append(integrations, i)
	}
	sortIntegrations(integrations)
	return integrations
}

func (e *setImpl) Get(id string) types.ImageRegistry {
	if id == "" {
		return nil
	}

	e.lock.RLock()
	defer e.lock.RUnlock()
	return e.integrations[id]
}

// GetAll returns the set of integrations that are active.
func (e *setImpl) GetAll() []types.ImageRegistry {
	e.lock.RLock()
	defer e.lock.RUnlock()
	return e.getSortedRegistriesNoLock()
}

// GetAllUnique returns the unique set of integrations that are active.
// Unique integrations are dependent on the integrations URL and auth information.
func (e *setImpl) GetAllUnique() []types.ImageRegistry {
	e.lock.RLock()
	defer e.lock.RUnlock()
	// Get the integrations sorted first. This ensures that we dedupe deterministically with the downside that
	// we iterate over the list of integrations twice.
	integrations := e.getSortedRegistriesNoLock()

	uniqueKeys := set.NewStringSet()
	uniqueIntegrations := make([]types.ImageRegistry, 0, len(integrations))
	// TODO(ROX-25474) refactor to pass parent context.
	ctx := context.Background()
	for _, i := range integrations {
		// If it's a user generated integration, we will not attempt to dedupe it.
		if !i.Config(ctx).Autogenerated {
			uniqueIntegrations = append(uniqueIntegrations, i)
			continue
		}

		uniqueKey := i.Config(ctx).URL + i.Config(ctx).Username + i.Config(ctx).Password +
			strconv.FormatBool(i.Config(ctx).Insecure)
		if !uniqueKeys.Contains(uniqueKey) {
			uniqueKeys.Add(uniqueKey)
			uniqueIntegrations = append(uniqueIntegrations, i)
		}
	}
	return uniqueIntegrations
}

// GetRegistryMetadataByImage returns the config for a registry that contains the input image.
func (e *setImpl) GetRegistryMetadataByImage(image *storage.Image) *types.Config {
	e.lock.RLock()
	defer e.lock.RUnlock()

	reg := e.getRegistryByImageNoLock(image)
	if reg != nil {
		// TODO(ROX-25474) refactor to pass parent context.
		return reg.Config(context.Background())
	}

	return nil
}

// GetRegistryByImage returns the registry that contains the input image.
func (e *setImpl) GetRegistryByImage(image *storage.Image) types.Registry {
	e.lock.RLock()
	defer e.lock.RUnlock()

	return e.getRegistryByImageNoLock(image)
}

func (e *setImpl) getRegistryByImageNoLock(image *storage.Image) types.Registry {
	if sourceID := image.GetMetadata().GetDataSource().GetId(); sourceID != "" {
		reg, ok := e.integrations[sourceID]
		if ok {
			return reg
		}
	}

	integrations := e.getSortedRegistriesNoLock()
	for _, i := range integrations {
		if i.Match(image.GetName()) {
			return i
		}
	}

	return nil
}

// Match returns whether a registry in the set has the given image.
func (e *setImpl) Match(image *storage.ImageName) bool {
	e.lock.RLock()
	defer e.lock.RUnlock()

	integrations := e.getSortedRegistriesNoLock()
	for _, i := range integrations {
		if i.Match(image) {
			return true
		}
	}
	return false
}

// IsEmpty returns whether the set is empty.
func (e *setImpl) IsEmpty() bool {
	e.lock.RLock()
	defer e.lock.RUnlock()

	return len(e.integrations) == 0
}

// Clear removes all present integrations.
func (e *setImpl) Clear() {
	e.lock.Lock()
	defer e.lock.Unlock()

	clear(e.integrations)
}

// UpdateImageIntegration updates the integration with the matching id to a new configuration.
// This does not update a pre-existing registry, instead it replaces it with a new one.
// Returns true if there was no pre-existing registry (an insert occurred), false otherwise.
func (e *setImpl) UpdateImageIntegration(integration *storage.ImageIntegration) (bool, error) {
	i, err := e.factory.CreateRegistry(integration, e.creatorOpts...)
	if err != nil {
		return false, err
	}

	e.lock.Lock()
	defer e.lock.Unlock()

	_, found := e.integrations[integration.GetId()]
	e.integrations[integration.GetId()] = i
	return !found, nil
}

// RemoveImageIntegration removes the integration with a matching id if one exists.
func (e *setImpl) RemoveImageIntegration(id string) error {
	e.lock.Lock()
	defer e.lock.Unlock()

	delete(e.integrations, id)
	return nil
}

func (e *setImpl) Len() int {
	e.lock.RLock()
	defer e.lock.RUnlock()

	return len(e.integrations)
}
